/*
 * Part of knowledge engineering (ke) course work, 2010/11
 */
package se.bth.ke.firstfriend.web;

import javax.ws.rs.QueryParam;
import se.bth.ke.firstfriend.model.dto.TopicInfo;
import java.util.Iterator;
import se.bth.ke.firstfriend.dto.BatchEditArticleDTO;
import de.nixis.commons.web.base.model.UserBase;
import de.nixis.commons.web.base.Controller;
import javax.persistence.Query;
import java.util.ArrayList;
import se.bth.ke.firstfriend.depend.ArticleDependencyManager;
import com.sun.jersey.api.core.InjectParam;
import java.util.Collections;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import se.bth.ke.firstfriend.model.Article;
import se.bth.ke.firstfriend.model.Comment;
import se.bth.ke.firstfriend.model.Topic;
import se.bth.ke.firstfriend.dto.ArticleWithHtml;
import com.sun.jersey.api.core.HttpContext;
import java.text.ParseException;
import java.util.Date;
import javax.annotation.security.RolesAllowed;
import javax.persistence.NoResultException;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status;
import se.bth.ke.firstfriend.model.Bookmark;
import se.bth.ke.firstfriend.model.BookmarkableItem;
import se.bth.ke.firstfriend.model.CheckPoint;
import se.bth.ke.firstfriend.model.Rating;
import se.bth.ke.firstfriend.model.Todo;
import se.bth.ke.firstfriend.model.User;
import se.bth.ke.firstfriend.dto.ActionStatus;
import se.bth.ke.firstfriend.dto.ArticleUserInfo;
import se.bth.ke.firstfriend.dto.EditArticleDTO;
import se.bth.ke.firstfriend.dto.FinishAddAsTodoDTO;
import se.bth.ke.firstfriend.util.Util;
import static se.bth.ke.firstfriend.util.Util.PRODUCES_HTML;

/**
 *
 * @author markus, nico.rehwaldt
 */
@Component
@Path("/article")
public class ArticleController extends Controller {
    
    @PersistenceContext
    private EntityManager em;

    @InjectParam
    private ArticleDependencyManager dependencyManager;

    @InjectParam
    private TopicController topicController;
    
    @GET
    @Path("/{id:[\\d]+}/edit")
    @RolesAllowed("admin")
    @Transactional
    public Object edit(@PathParam("id") Long articleId) {
        Article article = getArticle(articleId);
        return render("/article/edit", new EditArticleDTO(article, article.getTopics()).markAsUpdate());
    }

    @GET
    @Path("/batch/add")
    @RolesAllowed("admin")
    public Object batchCreate() {
        return render("/article/batch/add", new BatchEditArticleDTO(new EditArticleDTO(new Article(), ""), "Lorem ipsum"));
    }

    @GET
    @Path("/batch/addAsTodo")    
    @RolesAllowed("user")
    public Object batchAddAsTodo() {        
        List<TopicInfo> infos = topicController.getTopicInfos();
        
        Iterator<TopicInfo> iterator = infos.iterator();
        while (iterator.hasNext()) {
            TopicInfo info = iterator.next();
            
            List<Article> articles = em.createQuery(
                    "SELECT a FROM Topic t JOIN t.articles a " + 
                    "WHERE t = :topic AND " + 
                        "NOT EXISTS (SELECT t FROM Todo t WHERE t.article = a AND t.user = :user)")
                                .setParameter("topic", info.getTopic())
                                .setParameter("user", getActiveUser())
                                .getResultList();
            
            if (articles.isEmpty()) {
                iterator.remove();
            } else {
                info.setArticles(articles);
            }
        }
        
        return render("/article/batch/addAsTodo", infos);
    }
    
    @POST
    @Path("/batch")
    @RolesAllowed("admin")
    @Transactional
    public Object batchSave(
        @FormParam("title") List<String> titles,
        @FormParam("topics") List<String> articleTopics,
        @FormParam("id") List<Long> ids, 
        @FormParam("content") String content) {
        
        Iterator<String> titleIterator = titles.iterator();
        Iterator<String> topicsIterator = articleTopics.iterator();
        Iterator<Long> idsIterator = ids.iterator();
        
        List<Article> articles = new ArrayList();
        
        while (idsIterator.hasNext()) {
            Article article = createOrEditArticle(
                idsIterator.next(),
                titleIterator.next(), 
                content, 
                topicsIterator.next());
            
            articles.add(article);
        }
        
        em.flush();
        
        return render("/article/batch/save", articles);
    }
            
    @GET
    @Path("/create")
    @RolesAllowed("admin")
    public Object create() {
        return render("/article/edit", new EditArticleDTO(new Article(), ""));
    }

    @GET
    @Path("/list")
    public Object list() {
        return new ArrayList<Article>();
    }

    @POST
    @Transactional
    @RolesAllowed("admin")
    public Object save(
        @FormParam("title") String title,
        @FormParam("content") String content,
        @FormParam("topics") String topicNames,
        @FormParam("id") @DefaultValue("-1") long id) {

        Article article = createOrEditArticle(id, title, content, topicNames);        
        return redirect("/article/" + article.getSluggedId());
    }
    
    @POST
    @Transactional
    @RolesAllowed("user")
    @Path("/{id:[\\d]+}/rate")
    public ActionStatus rate(@PathParam("id") Long id, @FormParam("rating") int score) {
        Rating rating = new Rating(score);
        rating.setUser((User) getActiveUser());
        rating.setArticle(getArticle(id));

        em.persist(rating);

        return new ActionStatus("ok");
    }

    @GET
    @RolesAllowed("user")
    @Path("/{id:[\\d]+}/todo")
    @Produces(PRODUCES_HTML)
    public Object addAsTodo(@PathParam("id") Long id) {
        return render("/article/todo/create", getArticle(id));
    }
    
    @POST
    @Transactional
    @RolesAllowed("user")
    @Path("/{id:[\\d]+}/todo/ajax")
    @Produces(MediaType.APPLICATION_JSON)
    public Object ajaxSetAsTodo(
            @PathParam("id") Long id,
            @FormParam("deadline") @DefaultValue("") String deadline,
            @FormParam("priority") @DefaultValue("1") int priority) {
        
        try {
            Todo todo = createTodo(id, deadline, priority);
            return new ActionStatus("added", null, todo.getId());
        } catch (IllegalStateException e) {
            return new ActionStatus("already-exists");
        }        
    }
    
    private Todo createTodo(Long id, String deadline, int priority) {
        
        Article article = getArticle(id);
        Long todosOfArticle = getTodoCountFor(article);

        if (todosOfArticle > 0) {
             throw new IllegalStateException("Todo already exists");
        }
        
        Date date = null;

        try {
            date = Util.parseDate(deadline);
        } catch (ParseException e) {
            throw new WebApplicationException(e, Status.BAD_REQUEST);
        }
        
        Todo todo = new Todo(article, priority, date);
        todo.addCheckPoint(CheckPoint.workInProgress());
        
        todo.setUser((User) getActiveUser());
        em.persist(todo);
        
        return todo;
    }
    
    @POST
    @Transactional
    @RolesAllowed("user")
    @Path("/{id:[\\d]+}/todo")
    @Produces(PRODUCES_HTML)
    public Object setAsTodo(
            @PathParam("id") Long id,
            @FormParam("deadline") @DefaultValue("") String deadline,
            @FormParam("priority") @DefaultValue("1") int priority) {
        
        Todo todo;
        
        try {
            todo = createTodo(id, deadline, priority);
        } catch (IllegalStateException e) {
            todo = (Todo) em.createQuery("SELECT t FROM Todo t WHERE t.article.id = :id AND t.user = :user")
                            .setParameter("id", id)
                            .setParameter("user", getActiveUser())
                            .getSingleResult();
        }
                
        List<Article> alreadyTodo = em.createQuery("SELECT t.article FROM Todo t WHERE t.user = :user")
                                      .setParameter("user", getActiveUser())
                                      .getResultList();

        List<Article> suggestions = dependencyManager.getDependencySuggestions(todo.getArticle(), alreadyTodo);

        return render("/article/todo/finish", new FinishAddAsTodoDTO(todo, suggestions));
    }

    @GET
    @Transactional
    @RolesAllowed("user")
    @Path("/{id:[\\d]+}/bookmark")
    public Object setAsBookmark(@PathParam("id") Long id) {
        Article article = getArticle(id);        
        Long bookmarksOfArticle = getBookmarkCountFor(article);

        Bookmark bookmark;

        if (bookmarksOfArticle == 0) {
            bookmark = new Bookmark(article);
            bookmark.setUser((User) getActiveUser());
            em.persist(bookmark);
        } else {
            bookmark = (Bookmark)
                em.createQuery("SELECT b FROM Bookmark b WHERE b.article = :article AND b.user = :user ")
                  .setParameter("article", article)
                  .setParameter("user", getActiveUser())
                  .getSingleResult();
        }

        
        return render("/article/bookmark/created", bookmark);
    }

    @GET
    @Path("/{id:[\\d]+}-{slug}")
    @Produces(PRODUCES_HTML)
    public ArticleWithHtml show(@PathParam("id") long id, @Context HttpContext ctx) {        
        ArticleWithHtml info = getArticleWithHtml(getArticle(id), getActiveUser());
        info.setDependencies(dependencyManager.getDependencies(info.getArticle()));
        return info;
    }
    
    @POST
    @Path("/{articleId:[\\d]+}/comment")
    @Transactional
    public Object createComment(
            @PathParam("articleId") long articleId,
            @FormParam("author") String author, 
            @FormParam("comment") String commentString) {

        Article article = getArticle(articleId);
        Comment comment = new Comment(author, commentString);
        comment.setArticle(article);
        
        em.persist(comment);

        return redirect("/article/" + article.getSluggedId());
    }

    @POST
    @Path("{id:[\\d]+}/addDependencies")
    @Produces(PRODUCES_HTML)
    public Object addDependencies(
            @PathParam("id") long articleId,
            @FormParam("dependency") List<Long> dependencyIds) {

        Article article = getArticle(articleId);
        List<Article> dependencies = new ArrayList();

        for (Long id: dependencyIds) {
            Article dependency = em.find(Article.class, id);
            if (dependency == null) {
                throw new WebApplicationException(Status.NOT_FOUND);
            }
            dependencies.add(dependency);
        }
        
        dependencyManager.addDependencies(article, dependencies);
        return render("/article/dependency/save", article);
    }

    @GET
    @Path("{id:[\\d]+}/addDependencies")
    @Produces(PRODUCES_HTML)
    public Object showSetDependenciesDialog(@PathParam("id") long id) {
        Article article = getArticle(id);
        return render("/article/dependency/add", article);
    }

    private ArticleWithHtml getArticleWithHtml(Article article, UserBase user) {
        List<Comment> comments = getCommentsOf(article);        
        List<Topic> topics = getTopicsOf(article);
        ArticleWithHtml data = new ArticleWithHtml(article, comments, topics);
        
        if (user != null) {
            Rating rating = getRatingOf(article, user);
            BookmarkableItem bookmark = getBookmarkOf(article, user);
            Todo todo = getTodoOf(article, user);
            
            data.setUserInfo(new ArticleUserInfo(rating, bookmark, todo));
        }

        return data;
    }

    private Article getArticle(Long id) {
        Article article = em.find(Article.class, id);
        if (article == null) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }

        return article;
    }

    private Long getTodoCountFor(Article article) {
        return (Long) em.createQuery("SELECT COUNT(t) FROM Todo t WHERE t.user = :user and t.article = :article")
                        .setParameter("user", getActiveUser())
                        .setParameter("article", article)
                        .getSingleResult();
    }

    private Long getBookmarkCountFor(Article article) {
        return (Long) em.createQuery("SELECT COUNT(b) FROM Bookmark b WHERE b.user = :user and b.article = :article")
                        .setParameter("user", getActiveUser())
                        .setParameter("article", article)
                        .getSingleResult();
    }

    private List<Comment> getCommentsOf(Article article) {
        return em.createQuery("SELECT c FROM Comment c WHERE c.article = :article")
                 .setParameter("article", article)
                 .getResultList();        
    }

    private List<Topic> getTopicsOf(Article article) {
        return em.createQuery("SELECT t FROM Article a JOIN a.topics t WHERE a = :article")
                 .setParameter("article", article)
                 .getResultList();
    }

    private Rating getRatingOf(Article article, UserBase user) {
        try {
            return (Rating) em.createQuery("SELECT r FROM Rating r WHERE r.article = :article and r.user = :user ORDER BY r.createdAt DESC")
                              .setParameter("user", user)
                              .setParameter("article", article)
                              .setMaxResults(1)
                              .getSingleResult();

        } catch (NoResultException e) {
            return null;
        }
    }

    private BookmarkableItem getBookmarkOf(Article article, UserBase user) {
        try {
            return (BookmarkableItem) em.createQuery("SELECT b FROM Bookmark b WHERE b.article = :article and b.user = :user")
                                        .setParameter("user", user)
                                        .setParameter("article", article)
                                        .getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }

    private Todo getTodoOf(Article article, UserBase user) {
        try {
            return (Todo) em.createQuery("SELECT t FROM Todo t WHERE t.article = :article and t.user = :user")
                            .setParameter("user", user)
                            .setParameter("article", article)
                            .getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }

    private Article createOrEditArticle(Long id, String title, String content, String topicNames) {
        Article article;
        
        // This is an edit instead of a save
        if (id != -1) {
            article = getArticle(id);
            article.setContent(content);
            article.setTitle(title);
            article.setEditedAt(new Date());
        } else {
            article = new Article(title, content);
        }
        
        List<Topic> topics = Collections.emptyList();
        if (!topicNames.matches("[\\s]*")) {
            topics = getOrCreateTopics(topicNames.split("[\\s]*,[\\s]*"));
        }
        article.setTopics(topics);

        // Insert if save
        if (id == -1) {
            em.persist(article);
        }
        
        return article;
    }
    
    /**
     * Return a list of topics with the specified names.
     * If not existing they will be saved in the database prior return
     * @param topics
     * @return
     */
    private List<Topic> getOrCreateTopics(String... topics) {
        List<Topic> result = new ArrayList();

        Query q = em.createQuery("SELECT t FROM Topic t WHERE t.name = :name");

        for (String s : topics) {
            Topic t = null;
            String topicName = s.trim();

            try {
                t = (Topic) q.setParameter("name", topicName).getSingleResult();
            } catch (NoResultException e) {
                t = new Topic(topicName);
                em.persist(t);
                em.flush();
            }

            result.add(t);
        }
        return result;
    }
}
